using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Security.Principal;
using System.Runtime.Serialization;
using System.IO;
using System.Xml;

namespace SimpleBasicAuthentication
{
    /// <summary>
    /// Implements the security behind our Basic Authentication
    /// 
    /// based on http://blogs.msdn.com/b/astoriateam/archive/2010/07/21/odata-and-authentication-part-6-custom-basic-authentication.aspx
    /// Caching based on http://blogs.msdn.com/b/rjacobs/archive/2010/06/14/how-to-do-api-key-verification-for-rest-services-in-net-4.aspx
    /// 
    /// the credentials are stored in a file placed in the App_Data map
    /// it contains on each row the username, password and roles
    /// 
    /// <ArrayOfstring xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    /// <string>USER;userww;user</string>
    /// <string>ADMIN;adminww;admin</string>
    /// <string>BOTH;bothww;user|admin</string>
    /// </ArrayOfstring>
    /// </summary>
    public class BasicAuthenticationProvider
    {
        private const string C_USERCREDENTIALSLIST = "UserCredentialsList";

        /// <summary>
        /// Authenticates the specified context.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="checkForSsl">if set to <c>true</c> [check for SSL].</param>
        /// <returns>Authenticated?</returns>
        public static bool Authenticate(HttpContext context, bool checkForSsl)
        {
            List<string> userCredentials = context.Cache[C_USERCREDENTIALSLIST] as List<String>;

            if (userCredentials == null)
            {
                userCredentials = PopulateUserCredentials(context);
            }

            if (checkForSsl)
            {
                if (!HttpContext.Current.Request.IsSecureConnection)
                {
                    return false;
                }
            }

            if ((HttpContext.Current == null)
                || (HttpContext.Current.Request == null)
                || (HttpContext.Current.Request.Headers == null)
                || (HttpContext.Current.Request.Headers.AllKeys == null)
                || (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")))
            {
                return false;
            }

            string authHeader = HttpContext.Current.Request.Headers["Authorization"];

            IPrincipal principal;
            if (TryGetPrincipal(userCredentials, authHeader, out principal))
            {
                HttpContext.Current.User = principal;
                return true;
            }

            return false;
        }

        /// <summary>
        /// Populates the user credentials from file into the cache.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <returns>The list which is put into the cache</returns>
        private static List<String> PopulateUserCredentials(HttpContext context)
        {
            List<string> keyList;

            DataContractSerializer dcs = new DataContractSerializer(typeof(List<String>));
            var server = context.Server;
            using (FileStream fs = new FileStream(server.MapPath("~/App_Data/UserCredentials.xml"), FileMode.Open, FileAccess.Read))
            {
                using (XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas()))
                {
                    keyList = (List<String>)dcs.ReadObject(reader);
                }
            }

            // Save it in the cache
            context.Cache[C_USERCREDENTIALSLIST] = keyList;

            return keyList;
        }

        /// <summary>
        /// Tries to get the principal.
        /// </summary>
        /// <param name="userCredentials">The user credentials.</param>
        /// <param name="authHeader">The auth header.</param>
        /// <param name="principal">The principal or null.</param>
        /// <returns>Is authenticated?</returns>
        private static bool TryGetPrincipal(List<string> userCredentials, string authHeader, out IPrincipal principal)
        {
            var creds = ParseAuthHeader(authHeader);
            if ((creds != null)
                    && TryGetPrincipal(userCredentials, creds, out principal))
            {
                return true;
            }

            principal = null;
            return false;
        }

        /// <summary>
        /// Parses the auth header.
        /// </summary>
        /// <param name="authHeader">The auth header.</param>
        /// <returns></returns>
        private static string[] ParseAuthHeader(string authHeader)
        {
            // Check this is a Basic Auth header 
            if ( (authHeader == null)
                    || (authHeader.Length == 0)
                    || (!authHeader.StartsWith("Basic")))
            {
                return null;
            }

            // Pull out the Credentials with are seperated by ':' and Base64 encoded 
            string base64Credentials = authHeader.Substring(6);

            string[] credentials =
                Encoding.ASCII.GetString(
                  Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });

            if ((credentials.Length != 2)
                    || (string.IsNullOrEmpty(credentials[0]))
                    || (string.IsNullOrEmpty(credentials[1])))
            {
                return null;
            }

            // Okay these are the credentials 
            return credentials;
        }

        /// <summary>
        /// Tries the get principal.
        /// </summary>
        /// <param name="userCredentials">The user credentials from file.</param>
        /// <param name="creds">The creds entered by the user.</param>
        /// <param name="principal">The principal to return or null.</param>
        /// <returns>login succeeded?</returns>
        private static bool TryGetPrincipal(List<string> userCredentials, string[] creds, out IPrincipal principal)
        {
            foreach (string userCredentialString in userCredentials)
            {
                string[] userCredentialArray = userCredentialString.Split(';');

                string usernameEntered = creds[0];
                string passwordEntered = creds[1];
                if ((usernameEntered.ToUpper() == userCredentialArray[0].ToUpper())
                        && (passwordEntered == userCredentialArray[1]))
                {
                    string[] roles = userCredentialArray[2].Split('|');

                    principal = new GenericPrincipal(new GenericIdentity(userCredentialArray[0]), roles);
                    return true;
                }
            }

            principal = null;
            return false;
        }
    }
}
